package com.example.scancodelogindemo.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.IdUtil;
import com.example.scancodelogindemo.enums.QrCodeStatusEnum;
import com.example.scancodelogindemo.model.QrCodeInfo;
import com.example.scancodelogindemo.model.vo.QrCodeVO;
import com.example.scancodelogindemo.redisqueue.PublisherService;
import com.example.scancodelogindemo.redisqueue.TopicConstant;
import com.example.scancodelogindemo.service.QrcodeLoginService;
import com.example.scancodelogindemo.utils.QrcodeUtils;
import com.example.scancodelogindemo.utils.RedisGenerateKeyUtils;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.ExpiredObjectListener;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Objects;

/**
 * @author 傲寒
 * @date 2023/12/5
 */
@Slf4j
@Service
public class QrcodeLoginServiceImpl implements QrcodeLoginService {

    private static final String QR_STATE_KEY_PREFIX = "qrcode";

    private static final String QR_SCAN_KEY_PREFIX = QR_STATE_KEY_PREFIX + "scan_lock";

    private static final int QR_TIME_OUT = 5;

    private final RedissonClient redissonClient;


    private final PublisherService publisherService;

    public QrcodeLoginServiceImpl(RedissonClient redissonClient,
                                  PublisherService publisherService) {
        this.redissonClient = redissonClient;
        this.publisherService = publisherService;
    }

    /**
     * 创建二维码
     *
     * @return {@link String}
     */
    @Override
    public QrCodeVO createQrCode() {
        final String qrcodeId = IdUtil.getSnowflakeNextIdStr();
        final String qrCodeBase64str = QrcodeUtils.createQrCode(qrcodeId, 200, 200);
        final RBucket<QrCodeInfo> bucket = getQrCodeInfoBucket(qrcodeId);
        final QrCodeInfo infoDTO = new QrCodeInfo();
        infoDTO.setQrCodeId(qrcodeId);
        infoDTO.setQrCodeStatus(QrCodeStatusEnum.WAITING);
        bucket.set(infoDTO, Duration.ofMinutes(QR_TIME_OUT));
        return new QrCodeVO(qrcodeId, qrCodeBase64str);
    }

    /**
     * 检查二维码状态
     *
     * @param qrcodeId qrcode Id
     * @return {@link QrCodeInfo}
     */
    @Override
    public QrCodeInfo checkQrState(String qrcodeId) {
        final QrCodeInfo codeInfo = getQrCodeInfoBucket(qrcodeId).get();
        if (Objects.nonNull(codeInfo)) {
            return codeInfo;
        }
        return new QrCodeInfo(qrcodeId, QrCodeStatusEnum.INVALID);
    }


    /**
     * 扫描
     *
     * @param qrcodeId qrcode id
     * @return {@link Boolean}
     */
    @Override
    public Boolean scan(String qrcodeId, HttpServletRequest request) {
        String scan = buildKey(QR_SCAN_KEY_PREFIX, qrcodeId);
        final RLock lock = redissonClient.getLock(scan);
        try {
            final boolean triedLock = lock.tryLock();
            if (triedLock) {
                final RBucket<QrCodeInfo> codeInfoBucket = getQrCodeInfoBucket(qrcodeId);
                final QrCodeInfo qrCodeInfo = codeInfoBucket.get();
                sendOverdueMsg(qrcodeId, qrCodeInfo);
                Assert.notNull(qrCodeInfo, "二维码失效,请刷新页面");
                final String stateName = qrCodeInfo.getQrCodeStatus().name();
                Assert.isTrue(QrCodeStatusEnum.WAITING.name().equals(stateName), "二维码失效,请刷新页面");
                qrCodeInfo.setQrCodeStatus(QrCodeStatusEnum.SCANNED);
                codeInfoBucket.set(qrCodeInfo, Duration.ofMinutes(QR_TIME_OUT));
                publisherService.publishMessage(TopicConstant.QRCODE_STATE_CHANGE_TOPIC, qrcodeId);
                return true;
            }
            return false;
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 发送过期信息
     *
     * @param qrcodeId   qrcode id
     * @param qrCodeInfo qr码信息
     */
    private void sendOverdueMsg(String qrcodeId, QrCodeInfo qrCodeInfo) {
        if (Objects.isNull(qrCodeInfo)) {
            publisherService.publishMessage(TopicConstant.QRCODE_STATE_CHANGE_TOPIC, qrcodeId);
        }
    }

    /**
     * 确认
     *
     * @param qrcodeId qrcode id
     * @param request  请求
     */
    @Override
    public void confirm(String qrcodeId, HttpServletRequest request) {
        //获取app登录token
        //新生产一个token 放入缓存中
        String newToken = UUID.fastUUID().toString();
        final RBucket<QrCodeInfo> qrCodeInfoBucket = getQrCodeInfoBucket(qrcodeId);
        final QrCodeInfo qrCodeInfo = qrCodeInfoBucket.get();
        sendOverdueMsg(qrcodeId, qrCodeInfo);
        Assert.notNull(qrCodeInfo, "二维码失效,请刷新页面");
        final String stateName = qrCodeInfo.getQrCodeStatus().name();
        Assert.isTrue(QrCodeStatusEnum.SCANNED.name().equals(stateName), "二维码失效,请刷新页面");
        qrCodeInfo.setQrCodeStatus(QrCodeStatusEnum.CONFIRMED);
        qrCodeInfo.setToken(newToken);
        //设置到二维码信息中
        qrCodeInfoBucket.set(qrCodeInfo, Duration.ofMinutes(QR_TIME_OUT));
        publisherService.publishMessage(TopicConstant.QRCODE_STATE_CHANGE_TOPIC, qrcodeId);
    }


    /**
     * 取消
     *
     * @param qrcodeId qrcode id
     */
    @Override
    public void cancel(String qrcodeId) {
        final RBucket<QrCodeInfo> codeInfoBucket = getQrCodeInfoBucket(qrcodeId);
        final QrCodeInfo codeInfo = codeInfoBucket.get();
        if (Objects.nonNull(codeInfo)) {
            final String stateName = codeInfo.getQrCodeStatus().name();
            Assert.isTrue(QrCodeStatusEnum.SCANNED.name().equals(stateName), "二维码失效,请刷新页面");
            codeInfo.setQrCodeStatus(QrCodeStatusEnum.CANCEL);
            codeInfoBucket.set(codeInfo, Duration.ofMinutes(QR_TIME_OUT));
        }
        publisherService.publishMessage(TopicConstant.QRCODE_STATE_CHANGE_TOPIC, qrcodeId);
    }

    public RBucket<QrCodeInfo> getQrCodeInfoBucket(String qrcodeId) {
        final String cacheKey = buildKey(qrcodeId);
        final RBucket<QrCodeInfo> bucket = redissonClient.getBucket(cacheKey, JsonJacksonCodec.INSTANCE);
        bucket.addListenerAsync(new ExpiredObjectListener() {
            /**
             * 过期监听，发送消息提示二维码过期
             */
            @Override
            public void onExpired(String name) {
                log.info("getQrCodeInfoBucket#ExpiredObjectListener name:{}", name);
                publisherService.publishMessage(TopicConstant.QRCODE_STATE_CHANGE_TOPIC, qrcodeId);
            }
        });
        return bucket;
    }

    private static String buildKey(String qrcodeId) {
        return buildKey(QR_STATE_KEY_PREFIX, qrcodeId);
    }

    private static String buildKey(String prefix, String qrcodeId) {
        return RedisGenerateKeyUtils.buildKey(prefix, qrcodeId);
    }

}
